/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.util.Objects;

import java.net.InetAddress;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Time sensitive cache in charge of keeping track of the hostname. The {@code
 * InetAddress.getLocalHost().getCanonicalHostName()} call can be quite expensive and could be
 * called for the creation of each {@link SentryEvent}. This system will prevent unnecessary costs
 * by keeping track of the hostname for a period defined during the construction. For performance
 * purposes, the operation of retrieving the hostname will automatically fail after a period of time
 * defined by {@link #GET_HOSTNAME_TIMEOUT} without result.
 *
 * @since 2021-06-02
 */
final class HostnameCache {
    private static final long HOSTNAME_CACHE_DURATION = TimeUnit.HOURS.toMillis(5);

    /**
     * Time before the get hostname operation times out (in ms).
     */
    private static final long GET_HOSTNAME_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
    /**
     * Time for which the cache is kept.
     */
    private final long cacheDuration;
    /**
     * Current value for hostname (might change over time).
     */
    @Nullable
    private volatile String hostname;
    /**
     * Time at which the cache should expire.
     */
    private volatile long expirationTimestamp;
    /**
     * Whether a cache update thread is currently running or not.
     */
    private final @NotNull AtomicBoolean updateRunning = new AtomicBoolean(false);

    private final @NotNull Callable<InetAddress> getLocalhost;

    private final @NotNull ExecutorService executorService =
        Executors.newSingleThreadExecutor(new HostnameCacheThreadFactory());

    HostnameCache() {
        this(HOSTNAME_CACHE_DURATION);
    }

    HostnameCache(long cacheDuration) {
        this(cacheDuration, () -> InetAddress.getLocalHost());
    }

    /**
     * Sets up a cache for the hostname.
     *
     * @param cacheDuration cache duration in milliseconds.
     * @param getLocalhost a callback to obtain the localhost address - this is mostly here because of
     * testability
     */
    HostnameCache(long cacheDuration, final @NotNull Callable<InetAddress> getLocalhost) {
        this.cacheDuration = cacheDuration;
        this.getLocalhost = Objects.requireNonNull(getLocalhost, "getLocalhost is required");
        updateCache();
    }

    /**
     * Gets the hostname of the current machine.
     *
     * <p>Gets the value from the cache if possible otherwise calls {@link #updateCache()}.
     *
     * @return the hostname of the current machine.
     */
    @Nullable
    String getHostname() {
        if (expirationTimestamp < System.currentTimeMillis()
            && updateRunning.compareAndSet(false, true)) {
            updateCache();
        }

        return hostname;
    }

    /**
     * Force an update of the cache to get the current value of the hostname.
     */
    private void updateCache() {
        final Callable<Void> hostRetriever =
            () -> {
                try {
                    hostname = getLocalhost.call().getCanonicalHostName();
                    expirationTimestamp = System.currentTimeMillis() + cacheDuration;
                } finally {
                    updateRunning.set(false);
                }

                return null;
            };

        try {
            final Future<Void> futureTask = executorService.submit(hostRetriever);
            futureTask.get(GET_HOSTNAME_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            handleCacheUpdateFailure();
        } catch (ExecutionException | TimeoutException | RuntimeException e) {
            handleCacheUpdateFailure();
        }
    }

    private void handleCacheUpdateFailure() {
        expirationTimestamp = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(1);
    }

    /**
     * HostnameCacheThreadFactory
     *
     * @since 2021-06-02
     **/
    private static final class HostnameCacheThreadFactory implements ThreadFactory {
        private int cnt;

        @Override
        public @NotNull Thread newThread(final @NotNull Runnable runnable) {
            final Thread ret = new Thread(runnable, "SentryHostnameCache-" + cnt++);
            ret.setDaemon(true);
            return ret;
        }
    }
}
